﻿using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

namespace HDViewSL
{
	/// <summary>
	/// A representation of a multiresolution tiled panoramic image, derived from the data
	/// stored in an HD View XML file.
	/// </summary>
	public class HDViewImageSet
	{
		/// <summary>
		/// Constructs a new HD View image set.
		/// </summary>
		public HDViewImageSet()
		{
			// By default, assume perspective projection and 256 x 256 tiles with no overlap.
			this.Projection = HDViewProjection.Perspective;
			this.TileWidth = this.TileHeight = 256;
			this.MaxZoom = 2;
		}

		public Uri BaseUri { get; set; }
		public string UriPattern { get; set; }
		public int Levels { get; set; }
		public int Width { get; set; }
		public int Height { get; set; }
		public HDViewProjection Projection { get; set; }
		public double PhiMin { get; set; }
		public double PhiMax { get; set; }
		public double ThetaMin { get; set; }
		public double ThetaMax { get; set; }
		public double Roll { get; set; }
		public double Pitch { get; set; }
		public double Yaw { get; set; }
		public double MaxZoom { get; set; }
		public double MaxFieldOfView { get; set; }
		public int TileWidth { get; set; }
		public int TileHeight { get; set; }
		public int TileOverlap { get; set; }
		public int[] SubRect { get; set; }
		public string ColorProfile { get; set; }
		public bool AreTilesPadded { get; set; }		// KML PhotoOverlay image tiles are padded to be square.
		public bool IsLowerLeftOrigin { get; set; }		// KML PhotoOverlay may start with the bottom row of image tiles.

		internal int PaddedWidth { get; private set; }
		internal int PaddedHeight { get; private set; }

		private int FirstLevel { get; set; }
		private bool AreTilesGrouped { get; set; }		// Zoomify directories contain groups of 256 image tiles.
		private LevelInfo[] LevelInfoList { get; set; }

		/// <summary>
		/// Prepares the image set for use as a tile source.
		/// </summary>
		public void Initialize()
		{
			// Calculate first level number.
			var maxDimension = Math.Max(this.Width, this.Height);
			var deepZoomLevels = (int)Math.Ceiling(Math.Log(maxDimension, 2)) + 1;
			this.FirstLevel = Math.Max(0, deepZoomLevels - this.Levels);

			// See if the tiles are grouped.
			this.AreTilesGrouped = this.UriPattern.Contains("{g}");

			// If tiles are padded to be square, then round up the width and height
			// to the next larger power of two multiple of the tile size so that
			// Deep Zoom will blend between levels properly.
			this.PaddedWidth = this.Width;
			this.PaddedHeight = this.Height;
			if (this.AreTilesPadded)
			{
				int horizontalTileCount = (this.Width + this.TileWidth - 1) / this.TileWidth;
				int verticalTileCount = (this.Height + this.TileHeight - 1) / this.TileHeight;
				int maxTileCount = Math.Max(horizontalTileCount, verticalTileCount);
				int powerOfTwo = 1 << (int)Math.Ceiling(Math.Log(maxTileCount, 2));
				this.PaddedWidth = this.PaddedHeight = this.TileWidth * powerOfTwo;
			}


			// Store information about each level.
			this.LevelInfoList = new LevelInfo[this.Levels];
			var width = this.Width;
			var height = this.Height;
			var paddedWidth = this.PaddedWidth;
			var paddedHeight = this.PaddedHeight;
			for (var level = this.Levels - 1; level >= 0; level--)
			{
				this.LevelInfoList[level].HorizontalTileCount = (width + this.TileWidth - 1) / this.TileWidth;
				this.LevelInfoList[level].VerticalTileCount = (height + this.TileHeight - 1) / this.TileHeight;
				this.LevelInfoList[level].TotalTileCount = this.LevelInfoList[level].HorizontalTileCount * this.LevelInfoList[level].VerticalTileCount;
				this.LevelInfoList[level].PaddedHorizontalTileCount = (paddedWidth + this.TileWidth - 1) / this.TileWidth;
				this.LevelInfoList[level].PaddedVerticalTileCount = (paddedHeight + this.TileHeight - 1) / this.TileHeight;
				width /= 2;
				height /= 2;
				paddedWidth /= 2;
				paddedHeight /= 2;
			}
		}

		/// <summary>
		/// Gets the location of the image tile for the given level and position.
		/// </summary>
		/// <param name="tileLevel">The tile's level.</param>
		/// <param name="tilePositionX">The tile's horizontal position.</param>
		/// <param name="tilePositionY">The tile's vertical position.</param>
		/// <returns>The location of the specified tile.</returns>
		public Uri GetTileUri(int tileLevel, int tilePositionX, int tilePositionY)
		{
			tileLevel -= this.FirstLevel;

			// See if Deep Zoom is requesting a tile of lower resolution than we have available.
			if (tileLevel < 0)
			{
				// If the entire image fits into one tile at the lowest available resolution,
				// then use that tile (Deep Zoom will scale it to fit the right size).
				if (this.LevelInfoList[0].PaddedHorizontalTileCount == 1 && this.LevelInfoList[0].PaddedVerticalTileCount == 1)
				{
					tileLevel = tilePositionX = tilePositionY = 0;
				}
				else
				{
					return null;
				}
			}

			var uriString = this.UriPattern;

			// For a Zoomify image set, we replace "{g}" with the tile group number.
			// Each tile has a unique sequential index, and the tiles are stored
			// in directories with 256 tiles per group.
			if (this.AreTilesGrouped)
			{
				int tileIndex = this.LevelInfoList.Take(tileLevel).Sum(info => info.TotalTileCount) + tilePositionY * this.LevelInfoList[tileLevel].HorizontalTileCount + tilePositionX;
				int tileGroup = tileIndex / 256;
				uriString = uriString.Replace("{g}", tileGroup.ToString(CultureInfo.InvariantCulture));
			}

			// A KML PhotoOverlay may have its origin at the lower left corner of the image.  In that case,
			// we have to invert the tile's vertical index.
			if (this.IsLowerLeftOrigin)
			{
				tilePositionY = this.LevelInfoList[tileLevel].PaddedVerticalTileCount - 1 - tilePositionY;
			}

			// Reject requests for tiles beyond the actual image boundaries.  (These requests occur when we
			// pad a KML PhotoOverlay to a larger size than the actual image.)
			if (tilePositionX < 0 || tilePositionX >= this.LevelInfoList[tileLevel].HorizontalTileCount ||
				tilePositionY < 0 || tilePositionY >= this.LevelInfoList[tileLevel].VerticalTileCount)
			{
				return null;
			}

			uriString = uriString.Replace("{l}", tileLevel.ToString(CultureInfo.InvariantCulture));
			uriString = uriString.Replace("{L}", (tileLevel + 1).ToString(CultureInfo.InvariantCulture));
			uriString = uriString.Replace("{c}", tilePositionX.ToString(CultureInfo.InvariantCulture));
			uriString = uriString.Replace("{C}", (tilePositionX + 1).ToString(CultureInfo.InvariantCulture));
			uriString = uriString.Replace("{r}", tilePositionY.ToString(CultureInfo.InvariantCulture));
			uriString = uriString.Replace("{R}", (tilePositionY + 1).ToString(CultureInfo.InvariantCulture));

			// Remove "{*.zip}" -- HD View can read tiles from within a zip archive, but HD View SL cannot.
			uriString = Regex.Replace(uriString, @"\{[^}]*\.zip\}", String.Empty, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

			return new Uri(uriString, UriKind.Absolute);
		}

		/// <summary>
		/// Information about a single level of the multiresolution image pyramid.
		/// </summary>
		private struct LevelInfo
		{
			public int HorizontalTileCount { get; set; }
			public int VerticalTileCount { get; set; }
			public int TotalTileCount { get; set; }
			public int PaddedVerticalTileCount { get; set; }
			public int PaddedHorizontalTileCount { get; set; }
		}
	}
}
